package com.tuni.birthdaymoney.service.spring;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.mysema.query.types.expr.BooleanExpression;
import com.tuni.birthdaymoney.persistence.entity.Operation;
import com.tuni.birthdaymoney.persistence.entity.Person;
import com.tuni.birthdaymoney.persistence.entity.QOperation;
import com.tuni.birthdaymoney.persistence.repository.OperationRepository;
import com.tuni.birthdaymoney.persistence.repository.PersonRepository;
import com.tuni.birthdaymoney.service.OperationService;
import com.tuni.birthdaymoney.web.model.Donator;
import com.tuni.birthdaymoney.web.model.OperationHistoryPage;

@Service
public class SpringOperationService implements OperationService {

    @Autowired
    private OperationRepository operationRepository;

    @Autowired
    private PersonRepository personRepository;

    private void save(Operation operation) {
        operation.setDate(new DateTime());
        operationRepository.save(operation);
    }
    
    @Transactional
    public void saveDonation(Operation operation) {
        save(operation);
    }
    
    @Transactional
    public void saveMoneyBack(Operation operation) {
        operation.setAmount(operation.getAmount().negate());
        Iterable<Operation> allDonationsForThisPerson = operationRepository
                .findByReceiptorAndDonatorAndYear(operation.getReceiptor(), operation.getDonator(), operation.getYear());
        if (operation.getAmount().doubleValue() + getTotalDonatedAmount(allDonationsForThisPerson) >= 0) {
            save(operation);
        } else {
            operation.setAmount(operation.getAmount().negate());
            throw new IllegalArgumentException("Entered amount was greater then possible moneyback.");
        }
    }

    private double getTotalDonatedAmount(Iterable<Operation> allDonationsForThisPerson) {
       double totalAmount = 0;
       for (Operation donation : allDonationsForThisPerson) {
           totalAmount += donation.getAmount().doubleValue();
       }
       return totalAmount;
    }

    @Transactional(readOnly = true)
    public Iterable<Donator> getDonators(Person receiptor) {
        Iterable<Operation> operations = operationRepository.findByReceiptor(receiptor);
        
        List<Donator> donators = new ArrayList<Donator>();
        for (Operation operation : operations) {
            if (!donatorExists(donators, operation.getDonator().getId())) {
                Donator newDonator = new Donator(operation.getDonator());
                newDonator.setAmount(operation.getAmount().doubleValue());
                donators.add(newDonator);
            } else {
                Donator existedDonator = getById(donators, operation.getDonator().getId());
                double newAmount = existedDonator.getAmount() + operation.getAmount().doubleValue();
                existedDonator.setAmount(newAmount);
            }
        }

        return getDonatorsWithNonZeroAmount(donators);
    }
    
    @Transactional(readOnly = true)
    public Iterable<Donator> getDonators(Person receiptor, int year) {

        // TODO: review code duplication

        Iterable<Operation> operations = operationRepository.findByReceiptor(receiptor);

        List<Donator> donators = new ArrayList<Donator>();
        for (Operation operation : operations) {
            if (operation.getYear() == year) {
                if (!donatorExists(donators, operation.getDonator().getId())) {
                    Donator newDonator = new Donator(operation.getDonator());
                    newDonator.setAmount(operation.getAmount().doubleValue());
                    donators.add(newDonator);
                } else {
                    Donator existedDonator = getById(donators, operation.getDonator().getId());
                    double newAmount = existedDonator.getAmount() + operation.getAmount().doubleValue();
                    existedDonator.setAmount(newAmount);
                }
            }
        }

        return getDonatorsWithNonZeroAmount(donators);
    }

    @Transactional(readOnly = true)
    public double getTotalAmount(Person receiptor, int year) {
        Iterable<Operation> operations = operationRepository.findByReceiptorAndYear(receiptor, year);
        double total = 0;

        for (Operation operation : operations) {
            total += operation.getAmount().doubleValue();
        }

        return total;
    }

    private Iterable<Donator> getDonatorsWithNonZeroAmount(Iterable<Donator> donators) {
        List<Donator> donatorsWithoutZeros = new ArrayList<Donator>();
        for (Donator donator : donators) {
            if (donator.getAmount() != 0) {
                donatorsWithoutZeros.add(donator);
            }
        }
        return donatorsWithoutZeros;
    }

    private boolean donatorExists(Iterable<Donator> donators, int donatorId) {
        return (getById(donators, donatorId) != null);
    }

    private Donator getById(Iterable<Donator> donators, int donatorId) {
        for (Donator donator : donators) {
            if (donator.getId() == donatorId) {
                return donator;
            }
        }
        return null;
    }

    @Transactional(readOnly = true)
    public Operation findByID(Integer id) {
        return operationRepository.findById(id);
    }

    @Transactional(readOnly = true)
    public Iterable<Operation> findAll(BooleanExpression expression) {
        return operationRepository.findAll(expression);
    }

    @Transactional(readOnly = true)
    public Iterable<Operation> findAllOrderedByDate(BooleanExpression expression) {
        return operationRepository.findAll(expression, QOperation.operation.date.desc());
    }

    @Transactional(readOnly = true)
    public Iterable<Operation> findAll() {
        return operationRepository.findAll();
    }

    @Transactional(readOnly = true)
    public Iterable<Operation> findAllOrderedByDate() {
        return operationRepository.findAll(new Sort(new Order(Direction.DESC, "date")));
    }

    public Donator getDonatorById(Person receiptor, int id) {
        Iterable<Donator> donators = getDonators(receiptor);
        for (Donator donator : donators) {
            if (donator.getId() == id) {
                return donator;
            }
        }
        return null;
    }

    public Iterable<Integer> getAllYears() {
        List<Integer> yearList = new ArrayList<Integer>();
        // 10 years from current imho would be enough
        int currentYear = Calendar.getInstance().get(Calendar.YEAR);
        // allow not to chose any year
        yearList.add(null);
        int yearsAgo = 10;
        for (int i = 0; i < yearsAgo; i++) {
            yearList.add(currentYear - i);
        }
        return yearList;
    }

    public BooleanExpression prepareBooleanExpresion(OperationHistoryPage operationHistoryPageModel) {
        QOperation operation = QOperation.operation;
        BooleanExpression expression = null;
        if (operationHistoryPageModel.getDonator() != null
                && operationHistoryPageModel.getDonator().getId() != null)
            expression = operation.donator.id.eq(operationHistoryPageModel.getDonator().getId());
        if (operationHistoryPageModel.getReceiptor() != null
                && operationHistoryPageModel.getReceiptor().getId() != null) {
            expression = operation.receiptor.id.eq(operationHistoryPageModel.getReceiptor().getId()).and(expression);
        }
        if (operationHistoryPageModel.getStartDate() != null) {
            expression = operation.date.goe(operationHistoryPageModel.getStartDate()).and(expression);
        }
        if (operationHistoryPageModel.getEndDate() != null) {
            expression = operation.date.lt(operationHistoryPageModel.getEndDate().plusDays(1)).and(expression);
        }
        if (operationHistoryPageModel.getHumanResources() != null
                && operationHistoryPageModel.getHumanResources().getId() != null) {
            expression = operation.humanResources.id.eq(operationHistoryPageModel.getHumanResources().getId()).and(expression);
        }

        if (operationHistoryPageModel.getSelectedYear() != null) {
            expression = operation.year.eq(operationHistoryPageModel.getSelectedYear()).and(expression);
        }
        return expression;
    }
}